#ifndef URL_H_
#define URL_H_

#include <sstream>
#include <boost/ptr_container/ptr_list.hpp>

#include "exception.h"
#include "base64.h"

namespace gpyp {

class InvalidURL : public Exception {
public:
  inline InvalidURL(const std::string &url) :
    Exception("invalid url - " + url)
  {
  }
  inline InvalidURL(const std::string &msg, const std::string &url) :
    Exception(msg + " - " + url)
  {
  }
};

class URL {
public:
  struct Param {
    std::string  name_;
    std::string *value_;

    inline static void set_encoded(std::string &set,
                                   const char *s, const char *e) {
#define CHK_HEX(C) (((C) >= '0' && (C) <= '9') ||       \
                    ((C) >= 'a' && (C) <= 'f') ||       \
                    ((C) >= 'A' && (C) <= 'F'))
#define HEX2CHR(C) ((C) - ((C) >= 'a'                           \
                           ? 'a' - 10                           \
                           : ((C) >= 'A' ? 'A' - 10 : '0')))
      set.clear();
      for(const char *c = s; c < e; ++c) {
        if (*c == '%' && CHK_HEX(c[1]) && CHK_HEX(c[2])) {
          char h = HEX2CHR(c[1]) << 4 | HEX2CHR(c[2]);
          set.append(s, c);
          set += h;
          s = c + 3;
          c += 2;
        }
      }
      set.append(s, e);
#undef HEX2CHR
#undef CHK_HEX
    }

    inline void set_name_encoded(const char *s, const char *e) {
      set_encoded(name_, s, e);
    }
    inline void set_value_encoded(const char *s, const char *e) {
      if (s && s < e) {
        if (!value_) value_ = new std::string();
        set_encoded(*value_, s, e);
      } else if (value_) {
        delete value_;
        value_ = NULL;
      }
    }

    const Param &operator=(const Param &param) {
      if (value_) delete value_;
      name_  = param.name_;
      value_ = param.value_ ? new std::string(*param.value_) : NULL;
      return *this;
    }

    inline Param() :
      value_(NULL)
    {
    }
    inline Param(const std::string &name,
                 bool               decode = true) :
      value_(NULL)
    {
      if (decode) {
        set_name_encoded (name.c_str(),  name.c_str()  + name.size());
      } else {
        name_ = name;
      }
    }
    inline Param(const std::string &name,
                 const std::string &value,
                 bool               decode = true) :
      value_(NULL)
    {
      if (decode) {
        set_name_encoded (name.c_str(),  name.c_str()  + name.size());
        set_value_encoded(value.c_str(), value.c_str() + value.size());
      } else {
        name_  = name;
        value_ = new std::string(value);
      }
    }
    inline Param(const char *n_s, const char *n_e) :
      value_(NULL)
    {
      set_name_encoded(n_s, n_e);
    }
    inline Param(const char *n_s, const char *n_e,
                 const char *v_s, const char *v_e) :
      value_(NULL)
    {
      set_name_encoded(n_s, n_e);
      set_value_encoded(v_s, v_e);
    }
    inline Param(const Param &param) :
      name_(param.name_)
    {
      value_ = param.value_ ? new std::string(*param.value_) : NULL;
    }
    inline ~Param() {
      if (value_) delete value_;
    }
  };

  class ParamList : public boost::ptr_list<Param> {
  public:
    inline ParamList() { }
  };

private:
  std::string                scheme_;
  std::auto_ptr<std::string> id_;
  std::auto_ptr<std::string> pw_;
  std::string                host_;
  int                        port_;
  std::string                path_;

  std::auto_ptr<ParamList>   params_;

public:
  inline static std::string encode(const std::string &str) {
    char buf[4] = {'%', '\0', '\0', '\0'};
    std::string result;
    result.reserve(str.size());
    const char *l = str.c_str();
    for(const char *c = l; *c; ++c) {
      switch(*c) {
      case '!': case '\'': case '(': case ')':
      case '*': case '-': case '.': case '_': case '~':
        continue;
      }
      if ((*c >= '0' && *c <= '9') ||
          (*c >= 'A' && *c <= 'Z') ||
          (*c >= 'a' && *c <= 'z')) {
        continue;
      }

      result.append(l, c);
      unsigned char c1 = static_cast<unsigned char>(*c) / 16;
      unsigned char c2 = static_cast<unsigned char>(*c) % 16;
      buf[1] = c1 > 9 ? c1 - 10 + 'A' : c1 + '0';
      buf[2] = c2 > 9 ? c2 - 10 + 'A' : c2 + '0';
      result.append(buf);
      l = c + 1;
    }
    result.append(l);
    return result;
  }
  inline static void parse(const std::string &url,
                           std::string       *scheme,
                           std::string      **id,
                           std::string      **pw,
                           std::string       *host,
                           int               *port,
                           std::string       *path,
                           ParamList        **params) {
    const char *s = url.c_str(), *e = strchr(s, '#'), *c = s;
    const char *p_scheme = NULL;
    const char *p_id = NULL, *p_pw = NULL, *p_host = NULL;
    const char *p_path = NULL, *p_params = NULL;
    long i_port = -1;
    if (!e) e = s + url.size();
    for(; c < e && *c != ':'; ++c) {
      if ((*c < 'a' || *c > 'z') &&
          (*c < 'A' || *c > 'Z') &&
          (*c < '0' || *c > '9')) {
        throw InvalidURL("invalid scheme", url);
      }
    }
    if (c[0] != ':' || c[1] != '/' || c[2] != '/') {
      throw InvalidURL("scheme not specified", url);
    }
    p_scheme = c;
    c += 3;
    {
      for(; c < e && *c != '/'; ++c) {
        if (*c == ':') {
          p_host = c;
        } else if (*c == '@') {
          p_id   = p_host;
          p_pw   = c;
          p_host = NULL;
        }
      }
      if (p_host) {
        for(const char *p = p_host + 1; p < c; ++p) {
          if (*p < '0' || *p > '9') throw InvalidURL("invalid port", url);
        }
        char *end = NULL;
        i_port = strtol(p_host + 1, &end, 10);
        if (i_port < 0 || i_port > 65535 || end != c) {
          throw InvalidURL("port number out of range", url);
        }
      }
      for(const char *p = p_pw ? p_pw + 1 : p_scheme + 3;
          p < (p_host ? p_host : c); ++p) {
        if ((*p < 'a' || *p > 'z') &&
            (*p < 'A' || *p > 'Z') &&
            (*p < '0' || *p > '9') && *p != '-' &&  *p != '.') {
          throw InvalidURL("invalid host", url);
        }
      }
    }

    /* error check end */

    if (scheme) scheme->assign(s, p_scheme);
    if (id && p_pw) {
      *id = new std::string(p_scheme + 3, p_id ? p_id : p_pw);
    }
    if (pw && p_id && p_pw) {
      *pw = new std::string(p_id + 1, p_pw);
    }
    if (host) {
      host->assign(p_pw ? p_pw + 1 : p_scheme + 3, p_host ? p_host : c);
    }
    if (port) *port = i_port;

    if (path || params) {
      p_path = c;
      if (*c == '/') {
        for(; c < e; ++c) {
          if (*c == '?') {
            p_params = c;
            break;
          }
        }
      }
      if (path) {
        if (*p_path == '/') {
          path->assign(p_path, p_params ? p_params : e);
        } else {
          *path = "/";
        }
      }
      if (params && p_params) {
        *params = new ParamList();
        for(c = p_params + 1; c < e;) {
          const char *a = c, *s = NULL;
          for(; a < e && *a != '&'; ++a) {
            if (*a == '=') s = a;
          }
          if (s) {
            if (c < s) {
              (*params)->push_back(new Param(c, s, s + 1, a));
            }
          } else {
            (*params)->push_back(new Param(c, a));
          }
          c = a + 1;
        }
      }
    }
  }
  inline static std::ostream &write(std::ostream      &s,
                                    const std::string &scheme,
                                    const std::string *id,
                                    const std::string *pw,
                                    const std::string &host,
                                    int                port,
                                    bool               mask = false,
                                    bool               del  = false) {
    s << scheme << "://";
    if (id) {
      s << *id;
      if (pw) {
        s << ":" << (mask ? "******" : pw->c_str());
        if (del) delete pw;
      }
      s << "@";
      if (del) delete id;
    }
    s << host;
    if (port >= 0) s << ":" << port;
    return s;
  }
  inline static std::ostream &write_base(std::ostream      &s,
                                         const std::string &scheme,
                                         const std::string *id,
                                         const std::string *pw,
                                         const std::string &host,
                                         int                port,
                                         const std::string &path,
                                         bool               mask = false,
                                         bool               del  = false) {
    return write(s, scheme, id, pw, host, port, mask, del) << path;
  }
  inline static std::ostream &write_dir(std::ostream      &s,
                                        const std::string &scheme,
                                        const std::string *id,
                                        const std::string *pw,
                                        const std::string &host,
                                        int                port,
                                        const std::string &path,
                                        bool               mask = false,
                                        bool               del  = false) {
    write(s, scheme, id, pw, host, port, mask, del);
    std::string::size_type e = path.rfind('/');
    s.write(path.c_str(), e + 1);
    return s;
  }
  inline static std::ostream &write_full(std::ostream      &s,
                                         const std::string &scheme,
                                         const std::string *id,
                                         const std::string *pw,
                                         const std::string &host,
                                         int                port,
                                         const std::string &path,
                                         ParamList         *params,
                                         bool               mask = false,
                                         bool               del  = false);
  inline static std::string get_base(const std::string &url) {
    std::string scheme, *id = NULL, *pw = NULL, host, path;
    int port = -1;
    parse(url, &scheme, &id, &pw, &host, &port, &path, NULL);

    std::stringstream ss;
    write_base(ss, scheme, id, pw, host, port, path, false, true);
    return ss.str();
  }
  inline static std::string get_dir(const std::string &url) {
    std::string scheme, *id = NULL, *pw = NULL, host, path;
    int port = -1;
    parse(url, &scheme, &id, &pw, &host, &port, &path, NULL);

    std::stringstream ss;
    write_dir(ss, scheme, id, pw, host, port, path, false, true);
    return ss.str();
  }
  inline static std::string create_auth_base64(const std::string *id,
                                               const std::string *pw) {
    std::string str, res;
    str.reserve(1 + (id ? id->size() : 0) + (pw ? pw->size() : 0));
    if (id) str += *id;
    str += ':';
    if (pw) str += *pw;
    base64::encode(str, res);
    return res;
  }
  inline static bool validate(const std::string &url) {
    try {
      parse(url, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
      return true;
    } catch(const InvalidURL &/*ex*/) {
      return false;
    }
  }
  inline static URL build(const URL &url, const std::string &path) {
    URL res(url);
    res.set_relative_path(path);
    return res;
  }

  inline const std::string &get_scheme() const {
    return scheme_;
  }
  inline const std::string *get_id() const {
    return id_.get();
  }
  inline const std::string *get_pw() const {
    return pw_.get();
  }
  inline const std::string &get_host() const {
    return host_;
  }
  inline int get_port() const {
    return port_;
  }
  inline const std::string &get_path() const {
    return path_;
  }
  inline const ParamList *get_params() const {
    return params_.get();
  }

  inline bool is_dir() const {
    return path_.size() > 0 && path_.c_str()[path_.size() - 1] == '/';
  }

  inline void set(const std::string &url) {
    std::string *id = NULL, *pw = NULL;
    ParamList *params = NULL;
    parse(url, &scheme_, &id, &pw, &host_, &port_,
          &path_, &params);
    id_.reset(id);
    pw_.reset(pw);
    params_.reset(params);
  }
  inline void set(const URL &url) {
    scheme_ = url.scheme_;
    id_.reset(url.id_.get() ? new std::string(*url.id_) : NULL);
    pw_.reset(url.pw_.get() ? new std::string(*url.pw_) : NULL);
    host_ = url.host_;
    port_ = url.port_;
    path_ = url.path_;
    if (url.params_.get()) {
      params_.reset(new ParamList());
      for(ParamList::const_iterator iter = url.params_->begin();
          iter != url.params_->end(); ++iter) {
        params_->push_back(new Param(*iter));
      }
    } else {
      params_.reset();
    }
  }

  inline const std::string &operator=(const std::string &url) {
    set(url);
    return url;
  }
  inline const URL &operator=(const URL &url) {
    set(url);
    return *this;
  }

  inline void set_path(const std::string &path) {
    if (path.c_str()[0] == '/') {
      path_ = path;
    } else {
      path_ = "/";
      path_ += path;
    }
  }
  inline void add_path(const std::string &path, bool add_slash = true) {
    if (add_slash && !is_dir()) path_ += '/';
    path_ += path;
  }
  inline void erase_file() {
    if (!is_dir()) {
      std::string::size_type s = path_.rfind('/');
      path_.erase(s + 1);
    }
  }
  inline void set_relative_path(const std::string &path) {
    if (path.find("://") != std::string::npos) {
      set(path);
    } else if (path.c_str()[0] == '/') {
      set_path(path);
    } else {
      erase_file();
      add_path(path);
    }
  }

  inline void set_param(const std::string &name) {
    if (params_.get()) {
      for(ParamList::iterator iter = params_->begin();
          iter != params_->end(); ++iter) {
        if (iter->name_ == name) {
          if (iter->value_) {
            delete iter->value_;
            iter->value_ = NULL;
          }
          return;
        }
      }
    }
    if (!params_.get()) params_.reset(new ParamList());
    params_->push_back(new Param(name, false));
  }
  inline void set_param(const std::string &name, const std::string &value) {
    if (params_.get()) {
      for(ParamList::iterator iter = params_->begin();
          iter != params_->end(); ++iter) {
        if (iter->name_ == name) {
          if (!iter->value_) iter->value_ = new std::string();
          *iter->value_ = value;
          return;
        }
      }
    }
    if (!params_.get()) params_.reset(new ParamList());
    params_->push_back(new Param(name, value, false));
  }

  inline std::ostream &write(std::ostream &s, bool mask = false) const {
    write_full(s, scheme_, id_.get(), pw_.get(), host_, port_,
               path_, params_.get(), mask);
    return s;
  }

  inline std::string to_string() const {
    std::stringstream ss;
    write(ss);
    return ss.str();
  }
  inline std::string to_string_base() const {
    std::stringstream ss;
    write_base(ss, scheme_, id_.get(), pw_.get(), host_, port_, path_);
    return ss.str();
  }
  inline std::string to_string_dir() const {
    std::stringstream ss;
    write_dir(ss, scheme_, id_.get(), pw_.get(), host_, port_, path_);
    return ss.str();
  }

  inline bool has_auth() const {
    return id_.get() != NULL || pw_.get() != NULL;
  }
  inline std::string get_auth_base64() const {
    return create_auth_base64(id_.get(), pw_.get());
  }

  inline URL() { }
  inline explicit URL(const std::string &url) {
    set(url);
  }
  inline URL(const URL &url) {
    set(url);
  }
};

inline std::ostream &operator<<(std::ostream         &s,
                                const URL::ParamList &params) {
  bool first = true;
  for(URL::ParamList::const_iterator iter = params.begin();
      iter != params.end(); ++iter) {
    if (first) {
      s << "?";
      first = false;
    } else {
      s << "&";
    }
    s << URL::encode(iter->name_);
    if (iter->value_) s << "=" << URL::encode(*iter->value_);
  }
  return s;
}
inline std::ostream &operator<<(std::ostream &s, const URL &url) {
  url.write(s);
  return s;
}

inline std::ostream &URL::write_full(std::ostream      &s,
                                     const std::string &scheme,
                                     const std::string *id,
                                     const std::string *pw,
                                     const std::string &host,
                                     int                port,
                                     const std::string &path,
                                     ParamList         *params,
                                     bool               mask,
                                     bool               del) {
  write_base(s, scheme, id, pw, host, port, path, mask, del);
  if (params) {
    s << *params;
    if (del) delete params;
  }
  return s;
}

}/*gpyp*/

#endif/*URL_H_*/
